/*
 * Locations.java
 *
 * Copyright 2008 David D. Emory
 * 
 * This file is part of Five Points. See <http://www.fpdev.org> for
 * additional information regarding the project.
 * 
 * Five Points is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Five Points is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Five Points.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.fpdev.core.basenet;

import java.awt.geom.Point2D;
import java.io.File;
import java.io.FileWriter;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import org.fpdev.core.*;
import org.fpdev.util.*;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.fpdev.apps.rtemaster.GUIOutputAcceptor;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class Locations {

  private FPEngine engine_;
  private Map<String, String> aliasLookup_;
  private Map<Integer, String> zipCityLookup_;
  private Map<String, List<Integer>> cityZipLookup_;

  public Locations(FPEngine engine) {
    engine_ = engine;
    aliasLookup_ = new HashMap<String, String>();
    zipCityLookup_ = new HashMap<Integer, String>();
    cityZipLookup_ = new HashMap<String, List<Integer>>();
  }

  public List<Location> locationsNearCoordinate(Point2D.Double pt, double r) {

    System.out.println("Searching near point (" + pt.x + ", " + pt.y + ")");

    Set<String> locNames = new HashSet<String>();
    locNames.add("");
    List<LNCListNode> list = new LinkedList<LNCListNode>();
    
    //Iterator<BNode> nodes = engine_.getDB().getNearbyNodes(pt.x, pt.y, r).iterator();
    Collection<BNode> nodes = engine_.getDataPackage().getSpatialDataConn().nearbyNodes(pt.x, pt.y, r);
    System.out.println("nodes="+nodes);
    for(BNode node : nodes) { //nodes.hasNext()) {
      //BNode node = nodes.next();
      if (node.isStation()) {
        Location loc = new LocationLandmark(node.getStation().getName() + " MARTA Station", new LocationNode(node));
        if (!locNames.contains(loc.getDispText(false))) {
          list.add(new LNCListNode(loc, node.toPoint().distance(pt)));
          locNames.add(loc.getDispText(false));
        }
      } else if (engine_.getBaseNet().incidentLinks(node).size() > 2) {
        Location loc = new LocationIsect(node, engine_.getBaseNet());
        if (!locNames.contains(loc.getDispText(false)) && loc.getDispText(false).indexOf("&") >= 0) {
          list.add(new LNCListNode(loc, node.toPoint().distance(pt)));
          locNames.add(loc.getDispText(false));
        }
      }
    }
    System.out.println("Found " + list.size() + " results\n");
    
    Collections.sort(list);
    List locList = new LinkedList<Location>();
    for (Iterator<LNCListNode> it = list.iterator(); it.hasNext();)
      locList.add(it.next().getLocation());
    return locList;
  }

  private class LNCListNode implements Comparable {
    
    private Location loc_;
    private double dist_;
    
    public LNCListNode(Location loc, double dist) {
      loc_ = loc;
      dist_ = dist;
    }

    public int compareTo(Object o) {
      return new Double(dist_).compareTo(new Double(((LNCListNode) o).dist_));
    }
    
    public Location getLocation() { return loc_; }
  }
          
  public List<Location> resolveLocationFromInput(String input) {
    return resolveLocationFromInput(input, true);
  }

  public List<Location> resolveLocationFromInput(String input, boolean log) {

    //System.out.println("inp: "+input);
    input = input.split("\\,")[0];
    //System.out.println("inp: "+input);
    List<Location> list = resolveNonLandmarkLocation(input);

    //System.out.println("checking lm: "+input);
    /*Iterator<Location> lms = engine_.getLandmarksNew().resolveLandmarks(input).iterator();
    int c = 0;
    while (lms.hasNext()) {
      list.add(lms.next());
      c++;
    }
    System.out.println("  Resolved " + c + " landmark candidates");*/
    
    if (log) {
      engine_.getDataPackage().getCoreDB().logLocationQuery(0, 0, 1, input, list.size());
    }
    return list;
  }

  public List<Location> resolveNonLandmarkLocation(String input) {
    List<Location> list = new LinkedList<Location>();
    System.out.println("  Resolving non-landmark candidates");

    try {
      if (input.startsWith("%")) {
        //System.out.println("returning %");
        String id = input.substring(1).split("%")[0];
        BNode node = engine_.getNodeFromID(new Integer(id));
        if (node != null) {
          list.add(new LocationIsect(node, engine_.getBaseNet()));
          System.out.println("    Resolved as node " + node.getID());
        } else {
          engine_.msg("Invalid node ID: " + id);
        }
      }
      if (input.startsWith("@")) {
        //System.out.println("returning @");
        String[] split = input.substring(1).split("@");
        String id = split[0];
        BLink link = engine_.getLinkFromID(new Integer(id));
        if (link != null) {
          System.out.println("    Resolved as link " + link.getID());
          if (split.length > 1 && FPUtil.isInteger(split[1]) && link.getClassType() == BLink.CLASS_STREET) {
            int num = new Integer(split[1]).intValue();
            int zip = ((BLinkStreet) link).getZip(num);
            String city = this.getCityFromZip(zip);
            list.add(new LocationAddress(num + " " + link.getDisplayName(), (BLinkStreet) link, city, zip));
          } else {
            list.add(new LocationIsect(link.getFNode(), engine_.getBaseNet()));
          }
        } else {
          engine_.msg("Invalid link ID: " + id);
        }
      } else if (isAddress(input)) {
        System.out.println("    Identified as address");
        int addrNum = FPUtil.getAddressNumber(input);
        String addrSt = FPUtil.getAddressStreet(input);

        StreetNameParser splitter = new StreetNameParser(addrSt, this);
        ParsedStreetName ssn = splitter.getResolved();
        //System.out.println("ssn name: "+ssn.getName());
        ResultSet rs = engine_.getDB().queryStreetLinks(ssn.getName());
        while (rs.next()) {
          BLinkStreet link = (BLinkStreet) engine_.getLinkFromID(new Integer(rs.getInt(1)));
          splitter = new StreetNameParser(link.getDisplayName(), this);
          ParsedStreetName dbssn = splitter.getResolved();
          //System.out.println(" "+link.getDisplayText());
          boolean leftOK = link.getFAddrL() > 0 && link.getTAddrL() > 0 && ((link.getFAddrL() <= addrNum && addrNum <= link.getTAddrL()) || (link.getFAddrL() >= addrNum && addrNum >= link.getTAddrL()));
          boolean rightOK = link.getFAddrR() > 0 && link.getTAddrR() > 0 && ((link.getFAddrR() <= addrNum && addrNum <= link.getTAddrR()) || (link.getFAddrR() >= addrNum && addrNum >= link.getTAddrR()));
          if ((leftOK || rightOK) && ssn.compareTo(dbssn)) {
            System.out.println(link.getDisplayText());
            int zip = link.getZip(addrNum);
            String city = this.getCityFromZip(zip);
            list.add(new LocationAddress(addrNum + " " + link.getDisplayName(), link, city, zip));
            System.out.println("    Resolved as address: " + addrNum + " " + link.getDisplayName());
          }
        }

      } else if (isIntersection(input)) {
        System.out.println("    Identified as isect");
        String[] strArr = null;
        if (input.matches("[a-zA-Z0-9 .-]+ & [a-zA-Z0-9 .-]+")) {
          strArr = input.split(" & ");
        } else if (input.matches("[a-zA-Z0-9 .-]+ and [a-zA-Z0-9 .-]+")) {
          strArr = input.split(" and ");
        }

        this.resolveIsectPotentials(list, strArr[0], strArr[1]);

      }
    } catch (Throwable t) {
      System.out.println("Exception in resolveNonLandmarkLocation():");
      t.printStackTrace();
    }
    System.out.println("  Finished non-lm resolve attempt with " + list.size() + " candidates");
    return list;
  }

  public Location getResolvedLocation(String input) {
    System.out.println("gRL: " + input);
    return this.resolveLocationFromInput(input, false).get(0);
  }

  public Location getResolvedNonLandmarkLocation(String input) {
    return this.resolveNonLandmarkLocation(input).get(0);
  }

  public Iterator<BLinkStreet> getStreetLinksFromKey(String key) {
    List<BLinkStreet> list = new LinkedList<BLinkStreet>();
    try {
      ResultSet rs = engine_.getDB().queryStreetLinks(key);
      while (rs.next()) {
        BLinkStreet link = (BLinkStreet) engine_.getLinkFromID(new Integer(rs.getInt(1)));
        list.add(link);
      }
    } catch (SQLException ex) {
      ex.printStackTrace();
    }
    return list.iterator();
  }

  public boolean isAddress(String input) {
    return input.matches("[0-9]+ [a-zA-Z0-9 .-]+");
  }

  public boolean isIntersection(String input) {
    return input.matches("[a-zA-Z0-9 .-]+ & [a-zA-Z0-9 .-]+") || input.matches("[a-zA-Z0-9 .-]+ and [a-zA-Z0-9 .-]+");
  }

  public void buildIsectTable() {

    int i = 0;
    for(BNode node : engine_.getBaseNet().getNodes()) {
      i++;
      if (i % 1000 == 0) {
        System.out.println(i + " nodes processed");
      }
      List<String> streets = node.getIsectStreets("", engine_.getBaseNet());
      if (streets.size() >= 2) {

        ParsedStreetName ssn;

        while (streets.size() > 2) {
          ssn = new StreetNameParser(streets.get(0), this, false).getResolved();
          String name1 = ssn.getName();
          streets.remove(0);
          Iterator<String> remaining = streets.iterator();
          while (remaining.hasNext()) {
            ssn = new StreetNameParser(remaining.next(), this, false).getResolved();
            String name2 = ssn.getName();
            tryIsectKey(name1, name2, node);
          }
        }

        // do remaining pair
        ssn = new StreetNameParser(streets.get(0), this, false).getResolved();
        String name1 = ssn.getName();
        ssn = new StreetNameParser(streets.get(1), this, false).getResolved();
        String name2 = ssn.getName();
        tryIsectKey(name1, name2, node);
      }
    }
    engine_.getDB().flushInsertOps();
  }

  private void tryIsectKey(String name1, String name2, BNode node) {
    String isectKey = (name1.compareTo(name2) > 0) ? name2 + "@" + name1 : name1 + "@" + name2;

    // add to db

    engine_.getDB().initIsectKey(isectKey, node.getID());

  }
  private Set<Integer> nodeIDs_;

  public void resolveIsectPotentials(List<Location> loclist, String street1, String street2) {

    //System.out.println("rip1b: "+street1);
    //System.out.println("rip2b: "+street2);
    StreetNameParser ss1 = new StreetNameParser(street1, this, false);
    StreetNameParser ss2 = new StreetNameParser(street2, this, false);
    //System.out.println("rip1a: "+ssn1.getName());
    //System.out.println("rip2a: "+ssn2.getName());

    //List<CLocation> toRet = new LinkedList<CLocation>();
    nodeIDs_ = new HashSet<Integer>();
    Iterator<Location> iter;

    iter = resolveIsectPotentialsInternal1(ss1.getResolved(), ss2.getResolved(), engine_.getBaseNet());
    while (iter.hasNext()) {
      tryAddIsectLoc(loclist, iter.next());
    }
    if (ss1.areDistinct()) {
      //System.out.println("---- distinct: ss1");
      iter = resolveIsectPotentialsInternal1(ss1.getOriginal(), ss2.getResolved(), engine_.getBaseNet());
      while (iter.hasNext()) {
        tryAddIsectLoc(loclist, iter.next());
      }
    }
    if (ss2.areDistinct()) {
      //System.out.println("---- distinct: ss2");
      iter = resolveIsectPotentialsInternal1(ss1.getResolved(), ss2.getOriginal(), engine_.getBaseNet());
      while (iter.hasNext()) {
        tryAddIsectLoc(loclist, iter.next());
      }
    }
    if (ss1.areDistinct() && ss2.areDistinct()) {
      //System.out.println("---- distinct: ss1 & ss2");
      iter = resolveIsectPotentialsInternal1(ss1.getOriginal(), ss2.getOriginal(), engine_.getBaseNet());
      while (iter.hasNext()) {
        tryAddIsectLoc(loclist, iter.next());
      }
    }
  }

  private void tryAddIsectLoc(List<Location> loclist, Location loc) {
    Integer nodeID = new Integer(loc.getNode().getID());
    if (!nodeIDs_.contains(nodeID)) {
      loclist.add(loc);
      nodeIDs_.add(nodeID);
    }
  }

  private Iterator<Location> resolveIsectPotentialsInternal1(ParsedStreetName ssn1, ParsedStreetName ssn2, BaseNetwork net) {
    if (ssn1.getName().compareTo(ssn2.getName()) == 0) {
      Map<String, Location> toReturn = new HashMap<String, Location>();
      Iterator<Location> iter = resolveIsectPotentialsInternal2(ssn1, ssn2, net);
      while (iter.hasNext()) {
        Location isect = iter.next();
        if (!toReturn.containsKey(isect.getLocText())) {
          toReturn.put(isect.getLocText(), isect);
        }
      }
      iter = resolveIsectPotentialsInternal2(ssn2, ssn1, net);
      while (iter.hasNext()) {
        Location isect = iter.next();
        if (!toReturn.containsKey(isect.getLocText())) {
          toReturn.put(isect.getLocText(), isect);
        }
      }
      return toReturn.values().iterator();
    }
    return resolveIsectPotentialsInternal2(ssn1, ssn2, net);
  }

  private Iterator<Location> resolveIsectPotentialsInternal2(ParsedStreetName ssn1, ParsedStreetName ssn2, BaseNetwork net) {
    String name1 = ssn1.getName(), name2 = ssn2.getName();
    String isectKey = (name1.compareTo(name2) > 0) ? name2 + "@" + name1 : name1 + "@" + name2;
    //System.out.println("------ ikey: "+isectKey);
    List<Location> toReturn = new LinkedList<Location>();
    ResultSet rs = engine_.getDB().queryIsectKeys(isectKey);
    ParsedStreetName ssno, ssnr;
    try {
      while (rs.next()) {
        BNode node = (BNode) engine_.getNodeFromID(rs.getInt(2));

        List<String> streets = node.getIsectStreets("", net, false);
        Iterator<String> iter = streets.iterator();
        String str;
        do {
          str = iter.next();
          StreetNameParser ss = new StreetNameParser(FPUtil.standardizeStreetName(str, false), this, false);
          ssno = ss.getOriginal();
          ssnr = ss.getResolved();
        } while (ssno.getName().compareTo(name1) != 0 && ssnr.getName().compareTo(name1) != 0);
        boolean match1 = streetMatch(ssno, ssn1) || streetMatch(ssnr, ssn1);
        streets.remove(str);
        iter = streets.iterator();
        do {
          str = iter.next();
          StreetNameParser ss = new StreetNameParser(FPUtil.standardizeStreetName(str, false), this, false);
          ssno = ss.getOriginal();
          ssnr = ss.getResolved();
        } while (ssno.getName().compareTo(name2) != 0 && ssnr.getName().compareTo(name2) != 0);
        boolean match2 = streetMatch(ssno, ssn2) || streetMatch(ssnr, ssn2);

        if (match1 && match2) {
          toReturn.add(new LocationIsect(node, net));
        } //"%"+node.getID()+"%"+node.getIsectString("",graph));
      }
    } catch (SQLException ex) {
      ex.printStackTrace();
    }
    //System.out.println("------ returning: "+toReturn.size());
    return toReturn.iterator();
  }

  // TODO: deprecate following; use SSN.compareTo instead
  private boolean streetMatch(ParsedStreetName base, ParsedStreetName test) {
    if (test.getFacType().length() > 0 && test.getFacType().compareTo(base.getFacType()) != 0) {
      return false;
    }
    if (test.getDirPrefix().length() > 0 && test.getDirPrefix().compareTo(base.getDirPrefix()) != 0) {
      return false;
    }
    if (test.getQuadrant() > 0 && test.getQuadrant() != base.getQuadrant()) {
      return false;
    }
    if (test.getFacType().length() > 0 && test.getDirPrefix().compareTo(base.getDirPrefix()) != 0) {
      return false;
    }

    return true;
  }

  // STREET ALIAS METHODS
  public String resolveStreetAlias(String alias) {
    String result = aliasLookup_.get(alias);
    return result != null ? result : alias;
  }

  public boolean checkStreetAlias(String alias) {
    return aliasLookup_.containsKey(alias);
  }

  public void addStreetAlias(String alias, String target) {
    aliasLookup_.put(alias, target);
  }

  public void removeStreetAlias(String alias) {
    aliasLookup_.remove(alias);
  }

  public Iterator<String> getAliasesForStreet(String street) {
    street = street.trim().toLowerCase();
    List<String> toRet = new LinkedList<String>();
    Iterator<Entry<String, String>> entries = aliasLookup_.entrySet().iterator();
    while (entries.hasNext()) {
      Entry<String, String> entry = entries.next();
      if (entry.getValue().compareTo(street) == 0) {
        toRet.add(entry.getKey());
      }
    }
    return toRet.iterator();
  }

  public Iterator<Entry<String, String>> aliasEntries() {
    return aliasLookup_.entrySet().iterator();
  }

  public int aliasCount() {
    return aliasLookup_.size();
  }

  public void writeStreetAliasFile(String filename, GUIOutputAcceptor guiOA) {
    try {
      Iterator<Entry<String, String>> entries = aliasEntries();
      int totalItems = aliasCount(), numWritten = 0, pctWritten = 0, oldPctWritten;
      FileWriter writer = new FileWriter(filename);

      // write opening tags
      writer.write("<?xml version=\"1.0\"?>\n");
      writer.write("<fivepoints>\n");
      writer.write("<streetaliases>\n");

      while (entries.hasNext()) {
        Entry<String, String> entry = entries.next();
        writer.write("<alias name=\"" + entry.getKey() + "\" target=\"" + entry.getValue() + "\" />\n");

        // update progress bar
        numWritten++;
        oldPctWritten = pctWritten;
        double pctWrittenD = 100.0 * (double) numWritten / (double) totalItems;
        pctWritten = (int) pctWrittenD;
        if (pctWritten != oldPctWritten && guiOA != null) {
          guiOA.setProgress(pctWritten);
        }
      }

      // write closing tags
      writer.write("</streetaliases>\n");
      writer.write("</fivepoints>\n");
      writer.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public void readStreetAliasFile(String filename, FPEngine engine) {
    try {
      if(!(new File(filename)).exists()) {
        System.out.println("WARNING: Could not load streetalias.xml");
        return;
      }
      
      DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
      Document doc = docBuilder.parse(filename);
      NodeList docNodes = doc.getChildNodes();
      Node thisNode = docNodes.item(0);
      if (docNodes.getLength() != 1 && thisNode.getNodeName().equals("fivepoints")) {
        System.out.println("Not a valid Five Points datafile");
        return;
      }

      docNodes = thisNode.getChildNodes();

      for (int i = 0; i < docNodes.getLength(); i++) {
        thisNode = docNodes.item(i);
        String name = thisNode.getNodeName();
        //System.out.println("xmlnode: "+thisNode.getNodeName());
        if (name.equals("streetaliases")) {
          NodeList nodes = thisNode.getChildNodes();
          for (int ni = 0; ni < nodes.getLength(); ni++) {
            Node saNode = nodes.item(ni);
            if (saNode.getNodeName().equals("alias")) {
              NamedNodeMap attributes = saNode.getAttributes();
              String saName = attributes.getNamedItem("name").getNodeValue();
              String target = attributes.getNamedItem("target").getNodeValue();
              engine.getLocations().addStreetAlias(saName, target);
            }
          }
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  // ZIP/CITY METHODS
  public void readZipsFile(String filename) {
    try {
      if(!(new File(filename)).exists()) {
        System.out.println("WARNING: Could not load zips.xml");
        return;
      }
      DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
      Document doc = docBuilder.parse(filename);
      NodeList docNodes = doc.getChildNodes();
      Node thisNode = docNodes.item(0);
      if (docNodes.getLength() != 1 && thisNode.getNodeName().equals("fivepoints")) {
        System.out.println("Not a valid Five Points datafile");
        return;
      }

      docNodes = thisNode.getChildNodes();

      for (int i = 0; i < docNodes.getLength(); i++) {
        thisNode = docNodes.item(i);
        String name = thisNode.getNodeName();
        //System.out.println("xmlnode: "+thisNode.getNodeName());
        if (name.equals("zips")) {
          NodeList nodes = thisNode.getChildNodes();
          for (int ni = 0; ni < nodes.getLength(); ni++) {
            Node zNode = nodes.item(ni);
            if (zNode.getNodeName().equals("zip")) {
              NamedNodeMap attributes = zNode.getAttributes();
              int zipcode = new Integer(attributes.getNamedItem("code").getNodeValue()).intValue();
              String main = attributes.getNamedItem("main").getNodeValue();
              String alt = (attributes.getNamedItem("alt") != null) ? attributes.getNamedItem("alt").getNodeValue() : null;
              zipCityLookup_.put(zipcode, alt != null ? alt : main);
              addCityZipPair(main, zipcode);
              if (alt != null) {
                addCityZipPair(alt, zipcode);
              }
            //System.out.println("set "+zipcode+" to "+(alt != null ? alt : main));
            }
          }
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  private void addCityZipPair(String city, int zip) {
    city = city.toLowerCase();
    if (!cityZipLookup_.containsKey(city)) {
      cityZipLookup_.put(city, new LinkedList<Integer>());
    }
    cityZipLookup_.get(city).add(zip);
  }

  public String getCityFromZip(int zip) {
    if (!zipCityLookup_.containsKey(zip)) {
      return "";
    }
    return zipCityLookup_.get(zip);
  }
}


























